# Copyright (c) HySoP 2011-2024
#
# This file is part of HySoP software.
# See "https://particle_methods.gricad-pages.univ-grenoble-alpes.fr/hysop-doc/"
# for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import platform, math, itertools as it
from hysop.constants import System
from hysop.tools.units import bytes2str
from hysop.tools.contexts import printoptions
from hysop.tools.cache import machine_id
from hysop.backend.hardware.hwinfo import TopologyObject
from hysop.backend.hardware.cpu import CpuPackage
from hysop.backend.hardware.pci import PciBridge
[docs]
class NumaNode(TopologyObject):
"""
A set of processors around memory which the processors can directly access.
"""
def __init__(self, parent, node, package=None, bridge=None):
if node is not None:
self._packages = []
self._bridges = []
super().__init__(parent, node)
else:
assert package is not None
self._attributes = {}
self._packages = [package]
self._bridges = [bridge]
self.parent = parent
[docs]
def packages(self):
return sorted(self._packages, key=lambda x: x.os_index())
[docs]
def bridges(self):
return sorted(self._bridges, key=lambda x: x.os_index())
[docs]
def local_memory(self):
return self.attribute("local_memory")
[docs]
def node_set(self):
return self.attribute("nodeset")
[docs]
def node_mask(self):
nodeset = self.node_set()
mask_length = int(math.ceil(math.log(self.parent.full_node_set(), 2)))
_nodeset = "|{0:0{length}b}|".format(nodeset, length=mask_length)
_nodeset = _nodeset.replace("0", ".").replace("1", "x")
_nodeset += " 0x{0:0{length}x}".format(nodeset, length=mask_length // 4)
return _nodeset
[docs]
def cpu_packages_count(self):
return len(self._packages)
[docs]
def physical_cores_count(self):
return sum(x.physical_cores_count() for x in self._packages)
[docs]
def processing_units_count(self):
return sum(x.processing_units_count() for x in self._packages)
[docs]
def pci_bridge_count(self):
return len(self._bridges)
[docs]
def pci_devices_count(self):
return sum(x.pci_device_count() for x in self._bridges)
[docs]
def cpu_packages(self):
return self.packages()
[docs]
def pci_devices(self):
return it.chain.from_iterable([x.leaf_pci_devices() for x in self.bridges()])
def __str__(self):
return self.to_string()
[docs]
def to_string(self, expand_pci_tree=True):
header = f"-- NUMA Node {self.os_index()} --"
content = """
nodeset: {}
cpuset: {}
node memory: {}
""".format(
self.node_mask(),
self.cpu_mask(),
bytes2str(self.local_memory(), decimal=True),
)
for package in self.packages():
content += str(package)
if self.pci_bridge_count() > 0:
content += "\n::PCI bus topology::\n"
for bridge in self.bridges():
content += self.indent(
bridge.to_string(expand_pci_tree=expand_pci_tree)
)
return header + self.indent(content)
def _parsed_type(self):
return "NUMANode"
def _parse_object(self, it):
_type = it.attrib["type"]
if _type == "Package":
obj = CpuPackage(self, it)
self._packages.append(obj)
elif _type == "Bridge":
obj = PciBridge(self, it)
self._bridges.append(obj)
else:
raise ValueError(f"Unknown object type {_type}.")
[docs]
@classmethod
def from_package(cls, parent, package, bridge, attributes):
"""
Build a virtual NUMA node when there is only one socket.
"""
node = NumaNode(node=None, parent=parent, package=package, bridge=bridge)
node.update_attributes(attributes)
return node
[docs]
class Machine(TopologyObject):
"""
Class describing a physical machine (a set of processors and memory).
"""
def __init__(self, parent, machine):
if platform.system() == "Windows":
system = System.WINDOWS
elif platform.system() == "Darwin":
system = System.DARWIN
elif platform.system() == "Linux":
system = System.LINUX
else:
msg = f"Unknown platform system {platform.system()}."
raise ValueError(msg)
self._system = system
self._bridge, self._package = None, None
self._numa_nodes = []
super().__init__(parent, machine)
def _post_init(self):
if self._package:
self._attributes["nodeset"] = 1
attr = {
"local_memory": self.pop_attr("local_memory"),
"os_index": self.pop_attr("os_index"),
"cpuset": self.cpu_set(),
"nodeset": 1,
}
self._numa_nodes = [
NumaNode.from_package(
parent=self,
attributes=attr,
package=self._package,
bridge=self._bridge,
)
]
elif self._numa_nodes:
pass
else:
raise RuntimeError("Something went wrong during parsing.")
def _parsed_type(self):
return "Machine"
def _handle_child(self, child):
if child.tag == "page_type":
self._parse_page_type(child)
else:
super()._handle_child(child)
def _parse_page_type(self, it):
pass
def _parse_object(self, it):
_type = it.attrib["type"]
if _type == "Package":
assert self._package is None
self._package = CpuPackage(self, it)
elif _type == "Bridge":
assert self._bridge is None
self._bridge = PciBridge(self, it)
elif _type == "NUMANode":
self._numa_nodes.append(NumaNode(self, it))
else:
raise ValueError(
f"Unknown object type {_type} obtained during Machine parsing."
)
## Machine information
[docs]
def system(self):
return self._system
[docs]
def numa_nodes(self):
return sorted(self._numa_nodes, key=lambda x: x.os_index())
[docs]
def numa_nodes_count(self):
return len(self._numa_nodes)
[docs]
def distances(self):
if "distances" in self._attributes:
return self.attribute("distances")
else:
return None
[docs]
def node_set(self):
return self.attribute("nodeset")
[docs]
def full_node_set(self):
return self.node_set()
[docs]
def total_memory(self):
return sum(x.local_memory() for x in self._numa_nodes)
[docs]
def cpu_packages_count(self):
return sum(x.cpu_packages_count() for x in self._numa_nodes)
[docs]
def physical_cores_count(self):
return sum(x.physical_cores_count() for x in self._numa_nodes)
[docs]
def processing_units_count(self):
return sum(x.processing_units_count() for x in self._numa_nodes)
[docs]
def pci_devices_count(self):
return sum(x.pci_device_count() for x in self._numa_nodes)
[docs]
def cpu_packages(self):
return [
cpu
for cpu in it.chain.from_iterable(
[x.cpu_packages() for x in self.numa_nodes()]
)
]
[docs]
def pci_devices(self, vendor_id=None, device_id=None):
devices = it.chain.from_iterable([x.pci_devices() for x in self.numa_nodes()])
if vendor_id is not None:
devices = filter(lambda x: x.pci_system_vendor_id() == vendor_id, devices)
if device_id is not None:
devices = filter(lambda x: x.pci_system_device_id() == device_id, devices)
return tuple(devices)
[docs]
def architecture(self):
return self.attribute("architecture")
[docs]
def backend(self):
return "{} {}".format(self.attribute("backend"), self.attribute("architecture"))
[docs]
def os(self):
return "{} {} ({})".format(
self.attribute("os_name"),
self.attribute("os_release"),
self.attribute("os_version"),
)
[docs]
def bios(self):
return "{} v.{} ({})".format(
self.attribute("bios_vendor"),
self.attribute("bios_version"),
self.attribute("bios_date"),
)
[docs]
def board(self):
return "{} {} {}".format(
self.attribute("board_vendor"),
self.attribute("board_name"),
self.attribute("board_version"),
)
[docs]
def hwinfo_version(self):
return "{} (hwinfo) v.{}".format(
self.attribute("process_name"), self.attribute("hwloc_version")
)
def __str__(self):
return self.to_string(expand_pci_tree=True)
[docs]
def to_string(self, expand_pci_tree=True):
header = "== Physical Hardware Report =="
content = """
bios: {}
board: {}
board_id: {}
backend: {}
OS: {}
nodeset: 0x{:x}
cpuset: 0x{:x}
NUMA nodes: {}
CPU packages: {}
physical cores: {}
processing units: {}
physical memory: {}
""".format(
self.bios(),
self.board(),
machine_id,
self.backend(),
self.os(),
self.node_set(),
self.cpu_set(),
self.numa_nodes_count(),
self.cpu_packages_count(),
self.physical_cores_count(),
self.processing_units_count(),
bytes2str(self.total_memory(), decimal=True),
)
for node in self.numa_nodes():
content += "\n" + node.to_string(expand_pci_tree=expand_pci_tree) + "\n"
distances = self.distances()
if distances is not None:
content += "\nRelative latency matrix between NUMA nodes:"
with printoptions(precision=2):
content += f"\n{distances}"
content += "\n"
content += f"\nHardware info gathered with {self.hwinfo_version()}"
footer = "\n===================="
return header + self.indent(content) + footer